package com.michaelbulava.TFS.log;

import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.DefaultLogger;
import com.intellij.openapi.diagnostic.Logger;
import com.michaelbulava.TFS.TFSVcsApplicationComponent;
import org.apache.log4j.Level;
import org.jetbrains.annotations.NonNls;

import java.io.*;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Created by mbulava on 5/6/2015.
 */
public class FileLogger extends DefaultLogger {

    private static final String FILE_SUFFIX = ".log";
    private static final String FILE_PREFIX = "TFSVcs_";

    public static final String PREF_LOG_TO_FILE = TFSVcsApplicationComponent.PLUGIN_ID  + ";" + "WriteLoggingToFile";
    public static final String PREF_LOGGING_LOCATION = TFSVcsApplicationComponent.PLUGIN_ID  + ";" + "LoggingLocation";
    public static final String PREF_MINIMUM_LOGGING_LEVEL = "LoggingLevel";
    private static final String newLine = System.getProperty("line.separator");

    private static final String LOGGING_HEADER = "DATE/TIME\tCATEGORY\tTHREAD NAME\tTHREAD ID\tLEVEL\tMESSAGE\tTHROWABLE\tDETAILS"
            + newLine;

    private static final String LOGGING_FORMAT = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}" + newLine;
    private static final long MAX_LOG_FILE_SIZE = 102400;// Changed to 100K5242880; //5 MB


    private String category;
    private boolean logToFile;
    private String loggingLocation;
    private Level writeToFileLevel;

    private static File currentFile;
    private static final Object lock = new Object();
    private static final SimpleDateFormat logDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    private static class FileLoggerFactory implements Factory{

        @Override
        public Logger getLoggerInstance(String category) {
            return new FileLogger(category);
        }
    }

    private static Factory ourFactory = new FileLoggerFactory();

    public static Logger getInstance(@NonNls String category){
        return ourFactory.getLoggerInstance(category);
    }

    public static Logger getInstance(Class cl){
        return getInstance("#" + cl.getName());
    }

    public static final boolean getLogToFile(){
        try{
            PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
            return propertiesComponent.getBoolean(PREF_LOG_TO_FILE, false);
        } catch (NullPointerException e){
            return false;
        }
    }

    public static final void setLogToFile(boolean value){
        PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
        propertiesComponent.setValue(PREF_LOG_TO_FILE, Boolean.valueOf(value).toString());
    }

    public static final String getLoggingLocation(){
        try{
            PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
            return propertiesComponent.getValue(PREF_LOGGING_LOCATION, "");
        } catch(NullPointerException e) {
            return null;
        }
    }

    public static final void setLoggingLocation(String value){
        PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
        propertiesComponent.setValue(PREF_LOGGING_LOCATION,value);
    }

    public static final Level getMinimumLoggingLevel(){
        try{
            PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
            return Level.toLevel(Integer.parseInt(propertiesComponent.getOrInit(PREF_MINIMUM_LOGGING_LEVEL,
                    String.valueOf(Level.DEBUG_INT))));
        } catch(NullPointerException e){
            return Level.DEBUG;
        }
    }

    public static final void setMinimumLoggingLevel(Level value){
        PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
        propertiesComponent.setValue(PREF_MINIMUM_LOGGING_LEVEL, String.valueOf(value.toInt()));
    }

    public FileLogger(String category) {
        super(category);
        this.category = category;
        //PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();

        logToFile = getLogToFile();
        loggingLocation = getLoggingLocation();
        writeToFileLevel = getMinimumLoggingLevel();

        if ((loggingLocation == null || loggingLocation.length() == 0) && logToFile) {
            System.out.println("Logging Location is null or empty so logging to file will be disabled.");
            logToFile = false;
        }
    }

    @Override
    public boolean isDebugEnabled() {
        return writeToFileLevel.isGreaterOrEqual(Level.DEBUG);
    }

    @Override
    public void debug(String message) {
        super.debug(message);
        writeToFile(Level.DEBUG, message, null, (String[]) null);
    }

    @Override
    public void debug(Throwable t) {
        super.debug(t);
        writeToFile(Level.DEBUG, t.getMessage(), t, (String[])null);
    }

    @Override
    public void debug(String message, Throwable t) {
        super.debug(message, t);
        writeToFile(Level.DEBUG, message, t, (String[]) null);
    }

    @Override
    public void info(String message) {
        super.info(message);
        writeToFile(Level.INFO, message, null, (String[])null);
    }

    @Override
    public void info(String message, Throwable t) {
        super.info(message, t);
        writeToFile(Level.INFO, message, t, (String[])null);
    }

    @Override
    public void warn(String message, Throwable t) {
        super.warn(message, t);
        writeToFile(Level.WARN, message, t, (String[])null);
    }

    @Override
    public void error(String message, Throwable t, String... details) {
        //super.error(message, t, details);
        System.err.println("ERROR: " + message);
        if (t != null) t.printStackTrace(System.err);
        if (details.length > 0) {
            System.out.println("details: ");
            for (String detail : details) {
                System.out.println(detail);
            }
        }
        //if (t != null && (t.getMessage() == null || t.getMessage().length() == 0))
            //t = new Throwable("Throwable without Message",t);

        if (details.length == 0)
            details = null;
        writeToFile(Level.ERROR, message, t, details);
    }

    @Override
    public void setLevel(Level level) {
        super.setLevel(level);
    }


    private void writeToFile(long time, Level level, String message, String threadName, long threadId, Throwable t,
                             String... details){
        if (!logToFile || !level.isGreaterOrEqual(writeToFileLevel))
            return;
        synchronized (lock){
            try{
                if (currentFile == null)
                    setupNewFile();
                String output = MessageFormat.format(LOGGING_FORMAT, new Object[]{
                        logDateFormat.format(new Date(time)),
                        category,
                        threadName,
                        String.valueOf(threadId),
                        level.toString(),
                        message != null ? message : "",
                        getStackTraceString(t),
                        getDetailsString(details)
                });

                FileWriter fw = new FileWriter(currentFile, true);
                fw.write(output);
                fw.flush();
                fw.close();

                if (currentFile.length() >= MAX_LOG_FILE_SIZE)
                    currentFile = null;

            } catch (IOException e) {
                System.err.println("Failed to write to log file:");
                System.err.println(e);
            }
        }
    }

    private void writeToFile(final Level level, final String message, final Throwable t, final String... details){
        Thread currentThread = Thread.currentThread();
        final String threadName = currentThread.getName();
        final long threadId = currentThread.getId();
        final long currentTime = System.currentTimeMillis();
        //Spawn the file logger off to another thread, the wait locks seem to be killing performance of the entire application.
        ApplicationManager.getApplication().executeOnPooledThread(new Runnable(){

            @Override
            public void run() {
                writeToFile(currentTime, level, message, threadName, threadId, t, details);
            }

        });
//        synchronized (lock){
//            if (logToFile && level.isGreaterOrEqual(writeToFileLevel)){
//                try{
//                    if (currentFile == null)
//                        setupNewFile();
//                    Thread thread = Thread.currentThread();
//                    String output = MessageFormat.format(LOGGING_FORMAT, new Object[]{
//                            logDateFormat.format(new Date(System.currentTimeMillis())),
//                            category,
//                            thread.getName(),
//                            String.valueOf(thread.getId()),
//                            level.toString(),
//                            message != null ? message : "",
//                            getStackTraceString(t),
//                            getDetailsString(details)
//                    });
//
//                    FileWriter fw = new FileWriter(currentFile, true);
//                    fw.write(output);
//                    fw.flush();
//                    fw.close();
//
//                    if (currentFile.length() >= MAX_LOG_FILE_SIZE)
//                        currentFile = null;
//
//                } catch (IOException e) {
//                    System.err.println("Failed to write to log file:");
//                    System.err.println(e);
//                }
//            }
//        }
    }

    private void setupNewFile() throws IOException {
        File loggingFolder = new File(loggingLocation);
        if (!loggingFolder.exists())
            if (!loggingFolder.mkdirs())
                throw new IOException("Unable to create logging folder at [" + loggingLocation +"]");

        SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss") ;
        String fileName = FILE_PREFIX +
                fileDateFormat.format(new Date(System.currentTimeMillis())) + FILE_SUFFIX;
        File newFile = new File(loggingFolder, fileName);

        if (newFile.exists())
            throw new IOException("Generated already existing log file?");

        FileWriter fw = new FileWriter(newFile);
        fw.write(LOGGING_HEADER);
        fw.close();
        currentFile = newFile;
    }


    private String getStackTraceString(Throwable t){
        String exceptionString = "NULL";
        if (t != null){
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            t.printStackTrace(pw);
            exceptionString =sw.toString(); // stack trace as a string
            exceptionString = exceptionString.replace(newLine, "").replace("\t", " ^");
        }
        return exceptionString;
    }

    private String getDetailsString(String... details){
        String myDetails = "NULL";
        if (details != null && details.length > 0){
            for(int i =0; i < details.length; i++){
                myDetails +="[" + String.valueOf(i) + "]" + details[i] +";";
            }
            myDetails = myDetails.substring(0, myDetails.length()-1);
        }
        return myDetails;
    }
}
